/*file chiron src/http.js */
/*preamble

    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    The license terms are stated in full in <license.rst> and at the end
    of all source files.

*/

/*status works, tested in Firefox and Safari, requires work on "relative" */
/*quality .7 */
/*order 5 */

/**

    see "modules.js".

    "http.js" is a pseudo-module that "modules.js" requires
    to bootstrap the module loader.  You will find the
    bulk of the work in the HTTP module toward the end of
    "modules.js".  The following code augments the HTTP
    module so that it provides types and events consistent
    with the rest of the framework.

*/

/*todo ContentTooShortError */

include('base.js');
include('event.js');
include('optioned.js');
var environment = require('environment.js');
var modules = require('modules.js');

/*** logging
*/
exports.logging = false;

/* lastModifieds
    a lookup table the Last-Modified headers of received
    responses.
*/
var lastModifieds = {};

/*** request */
exports.request = function (options, observer) {
    var request = Request(options, observer);
    request.send();
    /* continuation passing or returning */
    if (request.isAsynchronous()) {
        return request;
    } else {
        return request.getResponse();
    }
};

/*** requestText */
exports.requestText = requestText;
/*** requestXml */
exports.requestXml = requestXml;
/*** requestDocument */
exports.requestDocument = requestDocument;

/*** resolve */
var baseResolve = resolve;
exports.resolve = foreignModuleBind(function (url, base) {
    if (no(base) && this.moduleScope && this.moduleScope.moduleUrl)
        base = baseResolve(this.moduleScope.moduleUrl, modules.modulesUrl);
    return baseResolve(url, base);
});

/*** relative */
var baseRelative = relative;
exports.relative = foreignModuleBind(function (url, base) {
    if (no(base) && this.moduleScope && this.moduleScope.moduleUrl)
        base = baseResolve(this.moduleScope.moduleUrl, modules.modulesUrl);
    return baseRelative(url, base);
});

/*** Request
    
    ``Request`` inherits from and replaces
    the prexisting ``Request`` defined
    in ``modules.js``.
*/
exports.BaseRequest = type(Request);
exports.Request = type([Optioned, Observable, BaseRequest], function (self, supr) {
    var method = "GET";
    var url, urlObject;
    var user;
    var password;
    var asynchronous;
    var ifModified;
    var content = '';
    var headers = list();

    /**** init
    */
    self.init = function (options, okObserver) {
        if (isInstance(options, String))
            options = {'url': options};
        supr.init(options);
        if (!!okObserver) {
            self.observe('ok', okObserver);
        }
    };

    /***** setIfModified
    */
    self.setIfModified = function (_ifModified) {
        ifModified = !!_ifModified;
        if (ifModified) {
            self.observe('ready', function (response) {
                /* track modification dates */
                if (ifModified) {
                    if (self.isNotModified()) {
                        self.notModified();
                    } else {
                        self.modified(response);
                    }
					try {
						var lastModified = realRequest.getResponseHeader("Last-Modified");
					} catch(exception) {
                        /* consume the exception thrown by Firefox if the header
                         * doesn't exist */
                    }
                    if (lastModified) {
                        lastModifieds[url] = lastModified;
                    }
                }

            });
        }
    };

    /***** getIfModified
    */
    self.getIfModified = function () {
        return ifModified;
    };

    /***** modified
    */
    self.modified = function () {
    };

    /***** notModified
    */
    self.notModified = function () {
    };

    /***** isNotModified
    */
    self.isNotModified = function () {
        try {
            var lastModified = self.getHeader("Last-Modified");
            var status = self.getStatus();
            return (
                /* normal */
                status == 304 ||
                /* Safari returns undefined when it hasn't changed */
                status === undefined && environment.isSafari ||
                /* check modification dates since Firefox rewrites 304 to 200 */
                lastModified == lastModifieds[url]
            );
        } catch (exception) {}
        return false;
    };

    /**** observe
        permits a user to observe `ready`, `ok`,
        `error`, and `warn` events with a handler
        function.  Observing any event on a `Request`
        causes the `open` and `send` to implicitly become
        asynchronous.
    */
    self.observe = function () {
        asynchronous = true;
        return supr.observe.apply(self, arguments);
    };

    /**** openOptions
        sets the ``method``, ``url``, ``asynchronous``,
        ``user``, and ``password`` options from `open`
        arguments, for as many arguments as are provided.
        This method is used by `open`.
    */
    self.openOptions = function () {
        /* set options */
        zip(
            ['method', 'url', 'asynchronous', 'user', 'password'],
            arguments
        ).eachArgs(function (key, value) {
            if (!no(value)) {
                self.setOption(key, value);
            }
        });
    };

    /**** open
    */
    self.open = function () {

        self.openOptions.apply(self, arguments);

        if (logging)
            log(relative(url, environment.url), 'load');

        /* open a connection */
        supr.open(
            method,
            url,
            asynchronous,
            user,
            password
        );

        /* send headers */
        headers.eachArgs(self.setHeader);

    };

    /**** send
        sends the `Request`.  If a content argument 
        is provided, it is sent.  Otherwise, if a ``content``
        option was specified for the `Request`, that content
        is sent.  Send implicitly opens the connection if
        that has not occured already.
    */
    self.send = function (_content) {
        if (!no(_content))
            self.setOption('content', _content);

        /* for the option: only send content if modified */
        if (self.getIfModified()) {
            self.setHeader(
                "If-Modified-Since",
                lastModifieds[url] || ('' + new Date(0))
            );
        }

        try {
            /* open if we haven't yet */
            if (!self.isOpen())
                self.open();
            /* defer to parent send */
            supr.send(content);
        } catch (exception) {
            /* subsume all errors under HTTP errors */
            throw new Error("HTTP Error: " + exception);
        }
    };

    /**** getProgress
        returns the percent progress of the download, or
        ``undefined`` if it is in progress but the progress
        is unknown.  The progress is determined by whether
        the response is ready, or how much data has been
        received so far and the response ``Content-length``
        header if it was provided.
    */
    self.getProgress = function () {
        var response = self.getResponse();
        if (!self.isSent()) {
            return 0;
        } else if (response.isReady()) {
            return 1;
        } else if (response.hasHeader('Content-length')) {
            return response.len() / response.getHeader('Content-length');
        } else {
            return undefined;
        }
    };

    /**** isSynchronous
        returns whether this is a synchronous HTTP request.
        `true` means that `send` will block until a response is ready;
        `false` means that `send` will not block and the response will be
        sent to an event listener like `ready`, `ok`, and `error`.
    */
    self.isSynchronous = function () {
        return !asynchronous;
    };

    /**** isAsynchronous
        returns whether this is an asynchronous HTTP request.
        `true` means that `send` will not block and the response will be
        sent to an event listener like `ready`, `ok`, and `error`;
        `false` means that `send` will block until a response is ready.
    */
    self.isAsynchronous = function () {
        return asynchronous;
    };

    /**** option method
    */
    self.option('method', function (value) {
        method = value;
    });

    /**** option url
    */
    self.option('url', function (value) {
        urlObject = parseUrl(resolve(value, window.location.href));
        if (urlObject.user)  {
            self.setOption('user', urlObject.user);
            urlObject.user = '';
        }
        if (urlObject.password) {
            self.setOption('password', urlObject.password)
            urlObject.password = '';
        }
        url = formatUrl(urlObject);
    });

    /**** option synchronous
    */
    self.option('synchronous', function (_synchronous) {
        if (no(_synchronous))
            _synchronous = true;
        asynchronous = not(_synchronous);
    });

    /**** option asynchronous
    */
    self.option('asynchronous', function (_asynchronous) {
        if (no(_asynchronous))
            _asynchronous = true;
        asynchronous = bool(_asynchronous);
    });

    /**** option user
    */
    self.option('user', function (_user) {
        user = string(_user);
    });

    /**** option password
    */
    self.option('password', function (_password) {
        password = string(_password);
    });

    /**** option content
    */
    self.option('content', function (_content) {
        content = string(_content)
    });

    /**** option header
    */
    self.option('header', function (_header) {
        headers.push(_header);
    });

    /**** option headers
    */
    self.option('headers', function (_headers) {
        headers.add(_headers);
    });

    /**** option timeout
    */
    self.option('timeout', function (_timeout) {
        self.setTimeout(number(_timeout));
    });

    /**** option ifModified
    */
    self.option('ifModified', self.setIfModified);

});

/*** Response

    ``Request`` inherits from and replaces
    the prexisting ``Request`` defined
    in ``modules.js``.
*/
exports.BaseResponse = type([], Response);
exports.Response = type([Event, BaseResponse], function (self, supr) {
    self.getHeaders = function () {
        return dict(supr.getHeaders());
    };
    self.getHeader = function (key) {
        var value = supr.getHeader(key);
        if (no(value)) new KeyError('HTTP Response Headers ' + key);
        return value;
    };
});

/*todo integrate this code for getXml for Explorer
    var xmlDocument = new ActiveXObject("Microsoft.XMLDOM");
    xmlDocument.loadXML(http.responseText);
*/
/*todo Response.getHtml */

/*** encode */
exports.encode = function (value) {
    if (no(value)) {
        return '';
    } else if (isInstance(value, String)) {
        return encodeComponent(value);
    } else if (isInstance(value, Number)) {
        return encodeComponent(value);
    } else {
        /* works for builtin arrays and objects and user defined
         * lists and dicts */
        return list(value).eachArgs(function (key, value) {
            if (!no(value)) {
                return encodeComponent(key) + '=' + encodeComponent(value);
            } else {
                return encodeComponent(key);
            }
        }).join('&');
    }
};

/*todo remove this bit of uncertainty about the existence of encodeURIComponent */
var encodeComponent;
if (window.encodeURIComponent) {
    encodeComponent = window.encodeURIComponent;
} else {
    warn("encodeURIComponent does not appear to be supported by this browser.");
    encodeComponent = function () {
        throw new Error("encodeComponent not supported with this browser");
    };
}

/*** keyLeave
*/
exports.keyLeaves = [
    'protocol', 'user', 'password', 'domains', 'port', 'root', 'directory',
    'file', 'query', 'anchor'
];

/*** looseExpression
*/
exports.looseExpression = new RegExp( /* url */
    "^" +
    "(?:" +
        "(?![^:@]+:[^:@/]*@)" + /* negative lookahead */
        "([^:/?#.]+)" + /* protocol */
        ":" +
    ")?" +
    "(?://)?" +
    "(" + /* authority */
        "(?:" +
            "(" + /* userInfo */
                "([^:@]*)" + /* user */
                ":?"
                + "([^:@]*)" + /* password */
            ")?" +
            "@" +
        ")?" +
        "([^:/?#]*)" + /* domain */
        "(?::(\\d*))?" + /* port */
    ")" +
    "(?:" +
        "(" + /* path */
            "(?:" +
                "(/)" + /* root */
                "(?:" +
                    "[^?#]" + /* directory */
                    "(?![^?#/]*\\.[^?#/.]+(?:[?#]|$))" +
                        /* negative lookahead */
                ")*" +
                "/?" +
            ")?" +
            "([^?#/]*)" + /* file */
        ")" +
        "(?:\\?([^#]*))?" + /* query */
        "(?:#(.*))?" + /* anchor */
    ")"
);

/*** parseLooseUrl
*/
exports.parseLooseUrl = urlParser(looseExpression);


/*license

    Legal
    =======
    
    Chiron is a component of the Tale web-game project.
    
    See <credit.txt> for a complete list of
    contributions and their licenses.  All contributions are provided
    under permissive, non-viral licenses including MIT, BSD, Creative Commons
    Attribution 2.5, Public Domain, or Unrestricted.
    
    
    License
    =======
    
    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    
    MIT License
    -----------
    
    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:
    
    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

*/

